#include "EncoderGC.h"
#include <cmath>
#include <cassert>
#include <cstdlib>
#include <zxing/common/IllegalArgumentException.h>
#include <newcode/ReedSolomonEncoder.h>


namespace newcode {
namespace encoder {

const int Encoder::FETCH_MASKS[] = {1, 3, 7};

const float Encoder::ERROR_CORECTION_LEVELS[] = {0.2, 0.4, 0.45, 0.25};

Encoder::Encoder(): dataIsString(false), pointer(0) {}

Ref<ByteMatrix> Encoder::encode(String data, EncodeMode encodeMode)
{
    ArrayRef<unsigned char> stringData = ArrayRef<unsigned char>(
        (unsigned char*)data.getText().data(), data.getText().size());
    dataIsString = true;
    return encode(stringData, encodeMode);
}

Ref<ByteMatrix> Encoder::encode(ArrayRef<unsigned char> data,
        EncodeMode encodeMode)
{
    //check if long holds at least 4 bytes.
    assert(sizeof(long) >= 4);
    this->currentStateOdd = true;
    this->initialDataSize = data.size();
    this->encodeMode = encodeMode;
    this->data = addErrorCorrectionBytes(data);
    if (initialDataSize > 0xFFFF)
    {
        throw IllegalArgumentException("Too large data to encode");
    }
    computeSizeInBarsels();
    setupFormatData();
    return buidMatrix();
}

int Encoder::messageSize()
{
    return 2 * ceil(ERROR_CORECTION_LEVELS[(int)encodeMode] *
                    initialDataSize) + initialDataSize;
}

void Encoder::computeSizeInBarsels()
{
    int dataCells;
    if (encodeMode != ENCODE_MODE_3_LEVELS)
    {
        dataCells = ceil((double) messageSize() / (encodeMode + 1));
    }
    else
    {
        dataCells = ceil((double) messageSize() / 1.5);
    }
    sizeInBarsels = ceil(sqrt((double)((dataCells + 9 + 1) >> 1))) * 4;
}

void Encoder::setupFormatData()
{
    formatBytes = ArrayRef<unsigned char>(3);
    formatBytes[0] = initialDataSize & 0xFF;
    formatBytes[1] = (initialDataSize & 0xFF00) >> 8;
    int isString = dataIsString? 1: 0;
    formatBytes[2] = ((int)encodeMode << 6) + isString;
}

Ref<ByteMatrix> Encoder::buidMatrix()
{
    Ref<ByteMatrix> result = Ref<ByteMatrix>(new ByteMatrix(sizeInBarsels + 2,
        sizeInBarsels + 2));
    for(int i = 0; i < sizeInBarsels + 2; i++)
    {
        (*result)(i, 0) = 0;
        (*result)(sizeInBarsels + 1, i) = 0;
        (*result)(0, i) = (i % 2 == 0)? 0: 255;
        (*result)(i, sizeInBarsels + 1) = (i % 2 == 1)? 0:255;
    }

    int hieght = sizeInBarsels / 4;
    int weigh = sizeInBarsels / 2;
    for(int w = 0; w < weigh; w++)
    {
        for(int h = 0; h < hieght; h++)
        {
            int absoluteH =  4 * h + 1;
            int absoluteW = 2 * w + 1;
            //left top corner of format
            if (h == 0 && w < 3)
            {
                setBarsels(result, absoluteH, absoluteW, getFormatElement(w));
            }
            else if (h == hieght - 2 && (w == 0 || w == weigh - 1))
            {
                setBarsels(result, absoluteH, absoluteW, getFormatElement(0));
            }
            else if (h == hieght - 1 && (w < 2 || w > weigh - 3))
            {
                if (w == 0 || w == weigh - 1)
                {
                    setBarsels(result, absoluteH, absoluteW,
                        getFormatElement(1));
                }
                else
                {
                    setBarsels(result, absoluteH, absoluteW,
                        getFormatElement(2));
                }
            } else {
                setBarsels(result, absoluteH, absoluteW, getNextElement());
            }
        }
    }

    return result;
}

ArrayRef<unsigned char> Encoder::getFormatElement(int index)
{
    ArrayRef<unsigned char> result(8);
    unsigned char byte = formatBytes[index] ^ MASK;
    for(int i = 7; i >= 0; i--)
    {
        result[i] = ((byte & 0x01) == 1)? 0: 255;
        byte >>= 1;
    }

    return result;
}

void Encoder::setBarsels(Ref<ByteMatrix> matrix, int h, int w,
    ArrayRef<unsigned char> barsels)
{
    for (int i = 0; i < 4; i++)
    {
        (*matrix)(h + i, w) = barsels[2 * i];
        (*matrix)(h + i, w + 1) = barsels[2 * i + 1];
    }
}

unsigned char Encoder::getNextMaskedByte()
{
    if (pointer < data.size())
    {
        return data[pointer++] ^ MASK;
    }
    else
    {
        return rand() ^ MASK;
    }
}

ArrayRef<unsigned char> Encoder::getNextElement()
{
    if (encodeMode != ENCODE_MODE_3_LEVELS)
    {
        ArrayRef<unsigned char> result(8);
        int bytesCount = (int)encodeMode + 1;
        long word = 0;
        for (int i = 0; i < bytesCount; i++)
        {
            word <<= 8;
            word |= getNextMaskedByte();
        }
        for(int i = 7; i >= 0; i--)
        {
            int val = word & FETCH_MASKS[bytesCount - 1];
            result[i] = barselForValue(val);
            word >>= (int)encodeMode + 1;
        }
        return result;
    }
    else
    {
        return getNextElement3Levels();
    }
}

ArrayRef<unsigned char> Encoder::getNextElement3Levels()
{
    ArrayRef<unsigned char> result(8);

    int index = 0, end = 0;
    if(currentStateOdd)
    {
        int buf = getNextMaskedByte() & 0xFF;
        buf <<= 8;
        buf |= getNextMaskedByte() & 0xFF;
        buf <<= 8;
        buf |= getNextMaskedByte() & 0xFF;
        digits = toTernarSystem(buf);
        index = 0;
        currentStateOdd = false;
    }
    else
    {
        index = 8;
        currentStateOdd = true;
    }
    end = index + 8;
    int i = 0;
    for (; index < end; index++)
    {
        result[i++] = barselForValue(digits[index]);
    }
    return result;
}

unsigned char Encoder::barselForValue(int value) const
{
    if (encodeMode != ENCODE_MODE_3_LEVELS)
    {
        float delta = 255.0f / ((2 << encodeMode) - 1);
        int h = (int)positiveRound(delta * value);
        return static_cast<unsigned char>(255 - h);
    }
    else
    {
        unsigned char greyVals[] = {255, 127, 0};
        return greyVals[value];
    }
}

float Encoder::positiveRound(float value) const
{
    return floor(value + 0.5f);
}

ArrayRef<unsigned char> Encoder::addErrorCorrectionBytes(
    const ArrayRef<unsigned char> initialData)
{
    ReedSolomonEncoder rse(GF256::QR_CODE_FIELD);
    size_t msgSize = messageSize();
    ArrayRef<int> toEncode(msgSize);
    for (size_t i = 0; i < msgSize; i++)
    {
        if (i < initialData.size()) {
            toEncode[i] = initialData[i];
        }
        else
        {
            toEncode[i] = 0;
        }
    }
    rse.encode(toEncode, msgSize - initialData.size());
    ArrayRef<unsigned char> res(msgSize);
    for (size_t i = 0; i < msgSize; i++)
    {
        res[i] = toEncode[i];
    }
    return res;
}

vector<int> Encoder::toTernarSystem(int val)
{
    vector<int> result(16);
    for (int i = 0; i < 16; i++)
    {
        result[15 - i] = val % 3;
        val /= 3;
    }
    return result;
}

}
}
